Release 10.1A: OpenEdge Development:
ProDataSets


Elements of a Business Entity

In principle, a Business Entity as an object can map to a Progress 4GL procedure. This might in turn be supported by any number of other procedures that provide either specific validation logic for the entity or general support services used by all entities. Each Business Entity is normally paired with a Data Access object that manages the connection to the actual data sources. As discussed earlier, in many cases a single running instance of a Data Access object could provide data for any number of requests from different Business Entity instances.

In a distributed stateless environment, a server-side Business Entity will normally not live beyond a single request. It can fill its ProDataSet for a particular Order, for example, return that Order to the client, and then either terminate or remain in memory to be used by another independent request, depending on how the application manages its procedures. On the other hand, if one Business Entity is used by another Business Entity in the same session, as part of its validation logic, for example, then there could be a relationship between the two procedures that would last beyond a single request.

As with the Data Access object, the Business Entity will typically be a static object. That is, it will be based on one or more static ProDataSet definitions, and contain (directly or indirectly) mostly specific logic to support its use. This logic will include a specific API for the various kinds of requests that can be made of it, and validation logic to apply to update requests. A dynamic Business Entity designed to handle a certain class of similar data objects is also entirely possible, of course.

Given these basics, the following subsections outline some of the common elements of the Business Entity.

ProDataSet and temp-table definitions

A Business Entity will typically contain the same temp-table and ProDataSet definitions as its Data Access object. The significant different is that it is the Business Entity’s instance of the ProDataSet that is actually filled with data used to satisfy a request, when it is passed BY-REFERENCE to its Data Access object.

Relationship to the Data Access object

If the application model is that an instance of the Data Access object serves a single Business Entity instance, then the Business Entity can run that instance as a persistent procedure, as in this example:

RUN OrderSource.p PERSISTENT SET hSourceProc. 

The Entity can then use the API of the Data Access object to run support methods in this procedure handle.

The Business Entity could also make the Data Access object a super procedure of the Business Entity, as shown:

THIS-PROCEDURE:ADD-SUPER-PROCEDURE(hSourceProc). 

This allows the Entity to use the API of the Data Access object as if it were part of its own procedure.

Using the super procedure technique also changes the nature of calls from outside that are routed from the Business Entity to the Data Access object. This is discussed in the "Data retrieval API" section.

Attaching the Data-Sources and callbacks

The entity procedure can, as part of its main block startup code, request that the Data Access object attach Data-Sources and set callback procedures to its ProDataSet instance, as in this example:

lError = DYNAMIC-FUNCTION("attachDataSet" IN hSourceProc, hDataSet). 

As was illustrated earlier, this can also be done in each of the methods of the Data Access object, rather than as a separate step.

Defining a data retrieval API

Any requests for data should be made to the Business Entity, never directly from outside to the Data Access object. This simply preserves the isolation of the different layers of the application. The fetchOrder procedure in OrderEntity.p is an example of this:

PROCEDURE fetchOrder: 
    DEFINE INPUT PARAMETER piOrderNum AS INTEGER NO-UNDO. 
    DEFINE OUTPUT PARAMETER DATASET FOR dsOrder BY-VALUE. 
    /* This turns around and runs an equivalent procedure in the 
    Data-Source procedure, passing in the static dataSet. */ 
    DYNAMIC-FUNCTION("attachDataSet" IN hSourceProc, hDataSet). 
    RUN fetchOrder IN hSourceProc (INPUT piOrderNum, 
        INPUT-OUTPUT DATASET dsOrder BY-REFERENCE). 
    DYNAMIC-FUNCTION("detachDataSet" IN hSourceProc, INPUT hDataSet). 
END PROCEDURE. /* fetchOrder */ 

This turns around and runs an equivalent procedure in the Data Access procedure. Significantly, the ProDataSet parameter is INPUT-OUTPUT BY-REFERENCE in the second-level call to the Data Access object. This uses the Business Entity’s ProDataSet and avoids the expense of copying the ProDataSet back and forth. Having avoided this, the overhead of the second procedure call is not very significant.

Figure 11–2 shows how the ProDataSet is being used in this case.

Figure 11–2: ProDataSet flow

Here is an outline of the steps illustrated in Figure 11–2:

  1. A requesting procedure on the client runs fetchOrder in the Order Business Entity on the server.
  2. That fetchOrder procedure runs fetchOrder in the Data Access object in its session.
  3. Because the ProDataSet is passed in BY-REFERENCE, it is actually the Business Entity’s instance that is used (marked in bold). The dotted lines indicate that this instance is passed without being copied.
  4. The fetchOrder procedure in the Data Access object attaches Data-Sources, which are tables in the database, and also sets any callback procedures for FILL events.
  5. It then does a FILL, which actually fills the ProDataSet instance back in the Business Entity.
  6. It returns the ProDataSet to the requesting procedure. It is copied there rather than being passed BY-REFERENCE because the presumption is that the requester is or may be in a different session.

Alternatively, if the Data Access object is a super procedure of the Business Entity, then in cases where the Business Entity version of a procedure like fetchOrder doesn’t do any additional work of its own, it could be dispensed with, and a call to fetchOrder from another procedure would be handled automatically by the Data Access object. In this case, there are a couple of things to consider:

Figure 11–3 illustrates what happens if the Data Access object is a super procedure of the Business Entity, and it has the only implementation of procedure fetchOrder.

Figure 11–3: Data Access object as super procedure

Here are the steps illustrated in Figure 11–3:

  1. The requesting procedure runs fetchOrder in the Business Entity as before.
  2. There is no fetchOrder procedure in the Business Entity. However, since the Data Access object is a super procedure, Progress runs fetchOrder there.
  3. This means that it is the Data Access object’s ProDataSet instance that is used for the request.
  4. The fetchOrder procedure attaches Data-Sources and callback procedures to this ProDataSet.
  5. It then fills the ProDataSet.
  6. The fetchOrder procedure then returns this ProDataSet to the original caller as an OUTPUT parameter. The ProDataSet instance in the Business Entity is not used.

This all works correctly in this case, but could be a source of confusion and errors because the ProDataSet instance being used is not consistent.

By contrast, Figure 11–4 shows the case where, once again, the Data Access object is a super procedure of the Business Entity, and fetchOrder in the Business Entity does a RUN SUPER to run the standard attach and fill behavior.

Figure 11–4: Data Access object as super procedure with RUN SUPER

Here are the steps illustrated in Figure 11–4:

  1. The requesting procedure runs fetchOrder in the Business Entity as before. There is an implementation of fetchOrder there, so it is executed.
  2. Procedure fetchOrder does a RUN SUPER, which runs fetchOrder in the Data Access object. Because the parameter definitions must be consistent in this case, the ProDataSet is simply an OUTPUT parameter.
  3. Because of this, the Data Access object uses its own ProDataSet instance.
  4. It attaches Data-Sources to its own ProDataSet instance.
  5. It fills its own ProDataSet instance.
  6. It returns its ProDataSet as an OUTPUT parameter to the Business Entity, copying it to the Business Entity’s ProDataSet instance.
  7. The Business Entity then returns it to the original caller as OUTPUT, again copying the ProDataSet.

Because of the extra copy operation, this is not a good configuration. This is something you need to keep in mind when you design your procedures and decide how they are related.

This discussion may seem complex, but the intention is to make you aware of some of the issues and how you should consider them when you’re designing your application. As with every other aspect of design, once you have thought through an appropriate solution to a part of your design, if you keep to your solution consistently, then you won’t have to worry about it anymore, and developers writing business logic don’t need to be concerned about the details of the Data Access architecture supporting them.

Defining a generic update API

As the sample procedures in earlier chapters show, you can create a general-purpose update API that can take changes made to any ProDataSet and apply them to the database, even executing validation procedures for the specific ProDataSet if they conform to a standard naming convention. This approach is very much like how the SmartDataObject and SmartBusinessObject in the ADM2 handle their update logic.

The generic update procedure in the samples is commitChanges.p. The sample entity procedure, OrderEntity.p, wraps this in a call of its own to provide an API for other procedures to use, as shown:

PROCEDURE saveChanges: 
    DEFINE INPUT-OUTPUT PARAMETER DATASET FOR dsOrder. 
    DEFINE VARIABLE hDataSet AS HANDLE     NO-UNDO. 
    hDataSet = DATASET dsOrder:HANDLE. 
    DYNAMIC-FUNCTION("attachDataSet" IN hSourceProc,  
                     INPUT hDataSet). 
    RUN commitChanges.p (INPUT-OUTPUT DATASET dsOrder BY-REFERENCE). 
    DYNAMIC-FUNCTION("detachDataSet" IN hSourceProc, 
                     INPUT hDataSet). 
END PROCEDURE. /* saveChanges */ 

This wrapper procedure attaches the Data-Sources and event handlers, runs commitChanges.p, and then detaches the Data-Sources. This could as easily be incorporated directly into a procedure like commitChanges if it knows where to run the attach and detach methods. Beyond that, having a wrapper procedure gives the specific object an opportunity to add special commit logic before or after the standard code.

Validation procedures for the generic update API

For example, the OrderEntity procedure has this sample validation procedure which based on its name will be executed whenever a row in the ttOline table in the ProDataSet is modified:

PROCEDURE ttOlineModify: 
    DEFINE INPUT PARAMETER DATASET FOR dsOrder. 
    /* If the customer doubled the quantity ordered, then 
       increase the discount by 20%. */ 
    IF ttOline.Qty >= (ttOlineBefore.Qty * 2) AND  
       ttOline.Discount = ttOlineBefore.Discount 
       THEN 
       ttOline.Discount = ttOlineBefore.Discount * 1.2. 
    ELSE IF ttOline.Qty <= (ttOlineBefore.Qty * .5) THEN 
        ASSIGN BUFFER ttOline:ERROR = YES 
               BUFFER ttOline:ERROR-STRING =  
                 "Line " + STRING(ttOline.LineNum) +  
                 ": You can't drop the Qty that much!". 
    RETURN. 
END PROCEDURE. /* ttOlineModify */ 

The basic principles of writing procedures such as this one are:

Validation procedures can set the ERROR and/or ERROR-STRING attributes for the row to communicate status information. Setting ERROR-STRING without setting error lets you return an informational or warning message without causing an actual error.

Defining a custom update API

In addition to using a standard update mechanism for general updates, there can of course be additional more specific update API calls in your Business Entity that handle particular situations where the default behavior is not sufficient.

Managing Business Entity instances

Managing Business Entities and their Data Access objects needs to be coordinated in some fashion, so that entities can locate each other, can be accessed from client requests, and can be started and stopped when appropriate.

In general, Business Entities and their APIs should be designed such that there are no dependencies between requests. This is especially necessary for a stateless distributed environment where a client cannot expect to be given access to the same procedure instance on an AppServer in successive calls. Thus, there is little reason to leave data in a Business Entity on the AppServer after a request completes.

This means that a Business Entity instance can be left running on an AppServer to service any number of unrelated requests. If the Business Entity procedures themselves are not enormous amount of r-code, it may be just as efficient to destroy each instance after its use and start a fresh instance when needed. It takes very little time to load a compiled procedure into memory. Any significant overhead is in startup code for the object, and this should be minimized. Keep in mind that while only one business instance is needed in an AppServer session to satisfy any number of successive requests from client sessions, the business logic within a session may need to use (and possibly start) other Business Entities to execute its own logic.

There should be a mapping between Business Entity names as used in business logic and actual procedure names, so that objects do not need to be aware of other procedure names to run them directly. This can be repository-based, and managed by a single session management utility within the session that accepts requests for entities and either locates or starts them and returns their handles. For example, an Order entity should be able to make a request of the Customer entity without having to know an actual procedure name to run. It should be the responsibility of the entity manager to handle this.

Support procedures that require only a single instance within a session, such as Data Access procedures, and most supporting business logic and update validation procedures, cam be started by the Business Entities or started by the entity management utility the first time they are needed, and then left running for the duration of the session, or else shut down on some form of LRU basis if memory becomes a problem.


Copyright © 2005 Progress Software Corporation
www.progress.com
Voice: (781) 280-4000
Fax: (781) 280-4095